// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using ILLink.RoslynAnalyzer;
using ILLink.Shared.TypeSystemProxy;
using Microsoft.CodeAnalysis;

#nullable enable
namespace ILLink.Shared.TrimAnalysis
{
    public sealed partial class FlowAnnotations
    {
        // In the analyzer there's no stateful data the flow annotations need to store
        // so we just create a singleton on demand.
        private static readonly Lazy<FlowAnnotations> _instance = new(() => new FlowAnnotations(), isThreadSafe: true);

        public static FlowAnnotations Instance { get => _instance.Value; }

        // Hide the default .ctor so that only the one singleton instance can be created
        private FlowAnnotations() { }

        public static bool RequiresDataFlowAnalysis(IMethodSymbol method)
        {
            if (GetMethodReturnValueAnnotation(method) != DynamicallyAccessedMemberTypes.None)
                return true;

            foreach (var param in method.GetParameters())
            {
                if (GetMethodParameterAnnotation(param) != DynamicallyAccessedMemberTypes.None)
                    return true;
            }

            return false;
        }

        internal static bool ShouldWarnWhenAccessedForReflection(ISymbol symbol) =>
            symbol switch
            {
                IMethodSymbol method => ShouldWarnWhenAccessedForReflection(method),
                IFieldSymbol field => ShouldWarnWhenAccessedForReflection(field),
                _ => false
            };

        private static bool ShouldWarnWhenAccessedForReflection(IMethodSymbol method)
        {
            bool? hasParameterAnnotation = null;
            if (GetMethodReturnValueAnnotation(method) == DynamicallyAccessedMemberTypes.None)
            {
                if (!HasParameterAnnotation(method))
                    return false;
                hasParameterAnnotation = true;
            }

            // If the method only has annotation on the return value and it's not virtual avoid warning.
            // Return value annotations are "consumed" by the caller of a method, and as such there is nothing
            // wrong calling these dynamically. The only problem can happen if something overrides a virtual
            // method with annotated return value at runtime - in this case the trimmer can't validate
            // that the method will return only types which fulfill the annotation's requirements.
            // For example:
            //   class BaseWithAnnotation
            //   {
            //       [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)]
            //       public abstract Type GetTypeWithFields();
            //   }
            //
            //   class UsingTheBase
            //   {
            //       public void PrintFields(Base base)
            //       {
            //            // No warning here - GetTypeWithFields is correctly annotated to allow GetFields on the return value.
            //            Console.WriteLine(string.Join(" ", base.GetTypeWithFields().GetFields().Select(f => f.Name)));
            //       }
            //   }
            //
            // If at runtime (through ref emit) something generates code like this:
            //   class DerivedAtRuntimeFromBase
            //   {
            //       // No point in adding annotation on the return value - nothing will look at it anyway
            //       // Trimming will not see this code, so there are no checks
            //       public override Type GetTypeWithFields() { return typeof(TestType); }
            //   }
            //
            // If TestType from above is trimmed, it may not have all its fields, and there would be no warnings generated.
            // But there has to be code like this somewhere in the app, in order to generate the override:
            //   class RuntimeTypeGenerator
            //   {
            //       public MethodInfo GetBaseMethod()
            //       {
            //            // This must warn - that the GetTypeWithFields has annotation on the return value
            //            return typeof(BaseWithAnnotation).GetMethod("GetTypeWithFields");
            //       }
            //   }

            return method.IsVirtual || method.IsOverride || (hasParameterAnnotation ?? HasParameterAnnotation(method));

            static bool HasParameterAnnotation(IMethodSymbol method)
            {
                foreach (var param in method.GetParameters())
                {
                    if (GetMethodParameterAnnotation(param) != DynamicallyAccessedMemberTypes.None)
                        return true;
                }
                return false;
            }
        }

        private static bool ShouldWarnWhenAccessedForReflection(IFieldSymbol field)
        {
            return GetFieldAnnotation(field) != DynamicallyAccessedMemberTypes.None;
        }

        internal static DynamicallyAccessedMemberTypes GetFieldAnnotation(IFieldSymbol field)
        {
            if (!field.OriginalDefinition.Type.IsTypeInterestingForDataflow(isByRef: field.RefKind is not RefKind.None))
                return DynamicallyAccessedMemberTypes.None;

            if (field.AssociatedSymbol is IPropertySymbol property
                && property.IsAutoProperty())
            {
                // If this is an auto property, we get the property annotation
                return property.GetDynamicallyAccessedMemberTypes();
            }

            return field.GetDynamicallyAccessedMemberTypes();
        }

        internal static DynamicallyAccessedMemberTypes GetBackingFieldAnnotation(IPropertySymbol property)
        {
            if (!property.OriginalDefinition.Type.IsTypeInterestingForDataflow(isByRef: false))
                return DynamicallyAccessedMemberTypes.None;

            return property.GetDynamicallyAccessedMemberTypes();
        }

        internal static DynamicallyAccessedMemberTypes GetTypeAnnotations(INamedTypeSymbol type)
        {
            DynamicallyAccessedMemberTypes typeAnnotation = type.GetDynamicallyAccessedMemberTypes();

            // Also inherit annotation from bases
            INamedTypeSymbol? baseType = type.BaseType;
            while (baseType is not null)
            {
                typeAnnotation |= baseType.GetDynamicallyAccessedMemberTypes();
                baseType = baseType.BaseType;
            }

            // And inherit them from interfaces
            foreach (INamedTypeSymbol interfaceType in type.AllInterfaces)
            {
                typeAnnotation |= interfaceType.GetDynamicallyAccessedMemberTypes();
            }

            return typeAnnotation;
        }

        internal static DynamicallyAccessedMemberTypes GetMethodParameterAnnotation(ParameterProxy param)
        {
            if (param.IsImplicitThis)
            {
                if (!param.Method.Method.ContainingType.IsTypeInterestingForDataflow(isByRef: false))
                    return DynamicallyAccessedMemberTypes.None;
                return param.Method.Method.GetDynamicallyAccessedMemberTypes();
            }

            IParameterSymbol parameter = param.ParameterSymbol!;
            bool isByRef = parameter.RefKind is not RefKind.None;
            if (!parameter.OriginalDefinition.Type.IsTypeInterestingForDataflow(isByRef))
                return DynamicallyAccessedMemberTypes.None;

            var damt = parameter.GetDynamicallyAccessedMemberTypes();

            IMethodSymbol parameterMethod = param.Method.Method;

            // If there are conflicts between the setter and the property annotation,
            // the setter annotation wins. (But DAMT.None is ignored)

            // Is this a property setter `value` parameter?
            if (parameterMethod!.MethodKind == MethodKind.PropertySet
                && damt == DynamicallyAccessedMemberTypes.None
                && parameter.Ordinal == parameterMethod.Parameters.Length - 1
                // Do not propagate property-level DAM for C# 14 extension properties
                && !parameterMethod.HasExtensionParameterOnType())
            {
                var property = (IPropertySymbol)parameterMethod.AssociatedSymbol!;
                Debug.Assert(property != null);
                damt = property!.GetDynamicallyAccessedMemberTypes();
            }

            return damt;
        }

        public static DynamicallyAccessedMemberTypes GetMethodReturnValueAnnotation(IMethodSymbol method)
        {
            if (!method.OriginalDefinition.ReturnType.IsTypeInterestingForDataflow(isByRef: method.ReturnsByRef))
                return DynamicallyAccessedMemberTypes.None;

            var returnDamt = method.GetDynamicallyAccessedMemberTypesOnReturnType();

            // Is this a property getter?
            // If there are conflicts between the getter and the property annotation,
            // the getter annotation wins. (But DAMT.None is ignored)
            if (method.MethodKind is MethodKind.PropertyGet
                && returnDamt == DynamicallyAccessedMemberTypes.None
                // Do not propagate property-level DAM for C# 14 extension properties]
                && !method.HasExtensionParameterOnType())
            {
                var property = (IPropertySymbol)method.AssociatedSymbol!;
                Debug.Assert(property != null);
                returnDamt = property!.GetDynamicallyAccessedMemberTypes();
            }

            return returnDamt;
        }

        public static DynamicallyAccessedMemberTypes GetTypeAnnotation(ITypeSymbol type)
        {
            var typeAnnotation = type.GetDynamicallyAccessedMemberTypes();

            ITypeSymbol? baseType = type.BaseType;
            while (baseType != null)
            {
                typeAnnotation |= baseType.GetDynamicallyAccessedMemberTypes();
                baseType = baseType.BaseType;
            }

            foreach (var interfaceType in type.AllInterfaces)
            {
                typeAnnotation |= interfaceType.GetDynamicallyAccessedMemberTypes();
            }

            return typeAnnotation;
        }

#pragma warning disable CA1822 // Mark members as static - the other partial implementations might need to be instance methods

        // TODO: This is relatively expensive on the analyzer since it doesn't cache the annotation information
        // For trimming tools this is an optimization to avoid the heavy lifting of analysis if there's no point
        // it's unclear if the same optimization makes sense for the analyzer.
        internal partial bool MethodRequiresDataFlowAnalysis(MethodProxy method)
            => RequiresDataFlowAnalysis(method.Method);

        internal partial MethodReturnValue GetMethodReturnValue(MethodProxy method, bool isNewObj, DynamicallyAccessedMemberTypes dynamicallyAccessedMemberTypes)
            => new MethodReturnValue(method.Method, isNewObj, dynamicallyAccessedMemberTypes);

        internal partial MethodReturnValue GetMethodReturnValue(MethodProxy method, bool isNewObj)
            => GetMethodReturnValue(method, isNewObj, GetMethodReturnValueAnnotation(method.Method));

        internal partial GenericParameterValue GetGenericParameterValue(GenericParameterProxy genericParameter, DynamicallyAccessedMemberTypes dynamicallyAccessedMemberTypes)
            => new GenericParameterValue(genericParameter.TypeParameterSymbol, dynamicallyAccessedMemberTypes);

        internal partial GenericParameterValue GetGenericParameterValue(GenericParameterProxy genericParameter)
            => new GenericParameterValue(genericParameter.TypeParameterSymbol);

        internal partial MethodParameterValue GetMethodThisParameterValue(MethodProxy method, DynamicallyAccessedMemberTypes dynamicallyAccessedMemberTypes)
        {
            if (!method.HasImplicitThis())
                throw new InvalidOperationException($"Cannot get 'this' parameter of method {method.GetDisplayName()} with no 'this' parameter.");
            return GetMethodParameterValue(new ParameterProxy(method, (ParameterIndex)0), dynamicallyAccessedMemberTypes);
        }

        internal partial MethodParameterValue GetMethodThisParameterValue(MethodProxy method)
        {
            if (!method.HasImplicitThis())
                throw new InvalidOperationException($"Cannot get 'this' parameter of method {method.GetDisplayName()} with no 'this' parameter.");
            ParameterProxy param = new(method, (ParameterIndex)0);
            var damt = GetMethodParameterAnnotation(param);
            return GetMethodParameterValue(new ParameterProxy(method, (ParameterIndex)0), damt);
        }

        internal MethodParameterValue GetMethodParameterValue(MethodProxy method, ParameterIndex parameterIndex, DynamicallyAccessedMemberTypes dynamicallyAccessedMemberTypes)
            => new MethodParameterValue(new(method, parameterIndex), dynamicallyAccessedMemberTypes);

        internal partial MethodParameterValue GetMethodParameterValue(ParameterProxy param)
            => new MethodParameterValue(param, GetMethodParameterAnnotation(param));

        internal partial MethodParameterValue GetMethodParameterValue(ParameterProxy param, DynamicallyAccessedMemberTypes dynamicallyAccessedMemberTypes)
            => new MethodParameterValue(param, dynamicallyAccessedMemberTypes);
#pragma warning restore CA1822
    }
}
